﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Drawing;
using System.Text;

using SharpGL.Media3D;

namespace SharpGL.Text
{
    /// <summary>
    /// The font outline format.
    /// </summary>
    public enum FontOutlineFormat
    {
        /// <summary>
        /// Render using lines.
        /// </summary>
        Lines = 0,

        /// <summary>
        /// Render using polygons.
        /// </summary>
        Polygons = 1
    }

    /// <summary>
    /// This class wraps the functionality of the wglUseFontOutlines function to
    /// allow straightforward rendering of text.
    /// </summary>
    public class FontOutlines
    {
        #region Internal Classes

        /// <summary>
        /// A FontOutline entry contains the details of a font face.
        /// </summary>
        internal class FontOutlineEntry
        {
            /// <summary>
            /// Gets or sets the list base.
            /// </summary>
            /// <value>
            /// The list base.
            /// </value>
            public uint ListBase
            {
                get;
                set;
            }

            /// <summary>
            /// Gets or sets the first glyph.
            /// </summary>
            /// <value>
            /// The first glyph.
            /// </value>
            public uint FirstGlyph
            {
                get;
                set;
            }

            /// <summary>
            /// Gets or sets the number of glyphs.
            /// </summary>
            /// <value>
            /// The number of glyphs.
            /// </value>
            public uint NumOfGlyphs
            {
                get;
                set;
            }

            /// <summary>
            /// Gets or sets the glyphs metric.
            /// </summary>
            /// <value>
            /// The glyphs metric.
            /// </value>
            public Win32.GLYPHMETRICSFLOAT[] GlyphMetrics
            {
                get;
                set;
            }

            /// <summary>
            /// Gets or sets the font.
            /// </summary>
            /// <value>
            /// The font.
            /// </value>
            public Font Font
            {
                get;
                set;
            }

            /// <summary>
            /// Gets or sets the deviation.
            /// </summary>
            /// <value>
            /// The deviation.
            /// </value>
            public float Deviation
            {
                get;
                set;
            }

            /// <summary>
            /// Gets or sets the extrusion.
            /// </summary>
            /// <value>
            /// The extrusion.
            /// </value>
            public float Extrusion
            {
                get;
                set;
            }

            /// <summary>
            /// Gets or sets the font outline format.
            /// </summary>
            /// <value>
            /// The font outline format.
            /// </value>
            public FontOutlineFormat FontOutlineFormat
            {
                get;
                set;
            }
        }

        #endregion

        #region Plivate Members

        private OpenGL currentOpenGLContext = null;

        private List<FontOutlineEntry> fontOutlineEntries = new List<FontOutlineEntry>();

        private uint firstFontGlyph = 0;
        private uint numOfFontGlyphs = 255;

        #endregion

        #region Private Methods

        private FontOutlineEntry CreateFontOutlineEntry(OpenGL gl, Font font, uint firstGlyph, uint numOfGlyphs,
            float deviation, float extrusion, FontOutlineFormat fontOutlineFormat)
        {
            uint listBase = 0;
            IntPtr hFont = IntPtr.Zero;
            IntPtr hOldObject = IntPtr.Zero;
            FontOutlineEntry foe = null;

            try
            {
                if (!OpenGL.IsValidContext(currentOpenGLContext) || null == font)
                    throw new System.ArgumentException("OpenGL context and font must not be null.");

                //  Make the OpenGL instance current.
                gl.MakeCurrent();

                //  Generate OpenGL list
                listBase = gl.GenLists((int)numOfGlyphs);
                if (0 == listBase)
                    throw new System.Exception("Can't generate new OpenGL display list.");

                //  Get font handle.
                hFont = font.ToHfont();

                //  Select the font handle.
                hOldObject = Win32.SelectObject(gl.RenderContextProvider.DeviceContextHandle, hFont);

                //  Create the array of glyphs metric.
                Win32.GLYPHMETRICSFLOAT[] gliphMetrics = new Win32.GLYPHMETRICSFLOAT[numOfGlyphs];                
                
                //  Create the font bitmaps.
                if (false == Win32.wglUseFontOutlines(gl.RenderContextProvider.DeviceContextHandle, firstGlyph, numOfGlyphs, listBase,
                                                            deviation, extrusion, (int)fontOutlineFormat, gliphMetrics))
                    throw new System.Exception("Can't create font outlines.");

                //  Reselect the old font.
                Win32.SelectObject(gl.RenderContextProvider.DeviceContextHandle, hOldObject);
                hOldObject = IntPtr.Zero;

                //  Free the font.
                Win32.DeleteObject(hFont);
                hFont = IntPtr.Zero;

                //  Create the font bitmap entry.
                foe = new FontOutlineEntry()
                {
                    ListBase = listBase,
                    FirstGlyph = firstGlyph,
                    NumOfGlyphs = numOfGlyphs,
                    GlyphMetrics = gliphMetrics,
                    Font = (Font)font.Clone(),
                    Deviation = deviation,
                    Extrusion = extrusion,
                    FontOutlineFormat = fontOutlineFormat
                };
            }
            catch
            {
                if (0 != listBase)
                    gl.DeleteLists(listBase, (int)numOfGlyphs);
                foe = null;
            }
            finally
            {
                if (IntPtr.Zero != hOldObject)
                    Win32.SelectObject(gl.RenderContextProvider.DeviceContextHandle, hOldObject);
                if (IntPtr.Zero != hFont)
                    Win32.DeleteObject(hFont);
            }

            return foe;
        }

        #endregion

        #region Construction/Destruction

        /// <summary>
        /// Constructor
        /// </summary>
        public FontOutlines()
        {
            currentOpenGLContext = null;
        }

        /// <summary>
        /// Destructor
        /// </summary>
        ~FontOutlines()
        {
            Destroy();
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates the outlines font in OpenGL instance.
        /// </summary>
        /// <param name="gl">The OpenGL instance.</param>
        /// <returns>True if successfully.</returns>
        public bool Create(OpenGL gl)
        {
            if (OpenGL.IsValidContext(currentOpenGLContext))
                return false;

            currentOpenGLContext = gl;

            return true;
        }

        /// <summary>
        /// Destroy the outlines font.
        /// </summary>
        public void Destroy()
        {
            if (OpenGL.IsValidContext(currentOpenGLContext))
            {
                foreach (FontOutlineEntry foe in fontOutlineEntries)
                {
                    currentOpenGLContext.DeleteLists(foe.ListBase, (int)foe.NumOfGlyphs);
                    ((IDisposable)foe.Font).Dispose();
                    foe.GlyphMetrics = null;
                }
            }

            fontOutlineEntries.Clear();

            currentOpenGLContext = null;
        }

        public void Dispose()
        {
            Destroy();

            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Draws the text.
        /// </summary>
        /// <param name="font">The font.</param>
        /// <param name="deviation">The deviation.</param>
        /// <param name="extrusion">The extrusion.</param>
        /// <param name="fontOutlineFormat">The font format.</param>
        /// <param name="text">The text.</param>
        public void DrawText(Font font, float deviation, float extrusion, FontOutlineFormat fontOutlineFormat, string text)
        {
            if (!OpenGL.IsValidContext(currentOpenGLContext) || null == text)
                return;

            //  Do we have a font outline entry for this OpenGL instance and font?
            var foeQuery = from foe in fontOutlineEntries
                            where foe.Font.Equals(font)
                            && foe.Deviation == deviation
                            && foe.Extrusion == extrusion
                            && foe.FontOutlineFormat == fontOutlineFormat
                           select foe;

            //  Get the FOE or null.
            FontOutlineEntry fontOutlineEntry = (foeQuery == null || foeQuery.Count() == 0 ? null : foeQuery.First());

            //  If we don't have the FOE, we must create it.
            if (fontOutlineEntry == null)
            {
                fontOutlineEntry = CreateFontOutlineEntry(currentOpenGLContext, font, firstFontGlyph, numOfFontGlyphs, deviation, extrusion, fontOutlineFormat);
                //  Add the font outline entry to the internal list.
                if (null != fontOutlineEntry)
                    fontOutlineEntries.Add(fontOutlineEntry);
            }

            // If we don't found FOE and can't create it do nothing
            if (null == fontOutlineEntry)
                return;

            //  Create an array of lists for the glyphs.
            List<uint> lists = new List<uint>();
            foreach (char c in text)
                lists.Add((uint)c);

            //  Check text for illegal characters.
            foreach (uint ch in lists)
            {
                int index = (int)ch - (int)fontOutlineEntry.FirstGlyph;
                if (0 > index || index >= fontOutlineEntry.NumOfGlyphs)
                    return;
            }

            //  Save current atributes
            currentOpenGLContext.PushAttrib(OpenGL.GL_LIST_BIT | OpenGL.GL_TRANSFORM_BIT | OpenGL.GL_POLYGON_BIT);
            
            //  Push modelview matrix.
            currentOpenGLContext.MatrixMode(OpenGL.GL_MODELVIEW);
            currentOpenGLContext.PushMatrix();
            
            //  Set the list base.
            currentOpenGLContext.ListBase(fontOutlineEntry.ListBase - fontOutlineEntry.FirstGlyph);

            //  Call the lists for the string.
            currentOpenGLContext.CallLists(lists.Count, lists.ToArray());
            //currentOpenGLContext.Flush();

            //  Push modelview matrix.
            currentOpenGLContext.PopMatrix();
            
            //  Restore atributes.
            currentOpenGLContext.PopAttrib();
        }

        public Rect3D GetTextSize(Font font, float deviation, float extrusion, FontOutlineFormat fontOutlineFormat, string text)
        {
            if (!OpenGL.IsValidContext(currentOpenGLContext) || null == font || null == text)
                return Rect3D.Empty;
            else if (text.Length == 0)
                return new Rect3D(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
            
            //  Do we have a font outline entry for this OpenGL instance and font?
            var foeQuery = from foe in fontOutlineEntries
                           where foe.Font.Equals(font)
                           && foe.Deviation == deviation
                           && foe.Extrusion == extrusion
                           && foe.FontOutlineFormat == fontOutlineFormat
                           select foe;

            //  Get the FOE or null.
            FontOutlineEntry fontOutlineEntry = (foeQuery == null || foeQuery.Count() == 0 ? null : foeQuery.First());

            //  If we don't have the FOE, we must create it.
            if (fontOutlineEntry == null)
            {
                fontOutlineEntry = CreateFontOutlineEntry(currentOpenGLContext, font, firstFontGlyph, numOfFontGlyphs, deviation, extrusion, fontOutlineFormat);
                //  Add the font outline entry to the internal list.
                if (null != fontOutlineEntry)
                    fontOutlineEntries.Add(fontOutlineEntry);
            }

            // If we don't found FOE and can't create it return empty size
            if (null == fontOutlineEntry)
                return Rect3D.Empty;

            //  Create an array of lists for the glyphs.
            List<uint> charList = new List<uint>();
            foreach (char c in text)
                charList.Add((uint)c);

            int glyphMetricIndex;

            glyphMetricIndex = (int)charList[0] - (int)fontOutlineEntry.FirstGlyph;
            if (0 > glyphMetricIndex || glyphMetricIndex >= fontOutlineEntry.NumOfGlyphs)
            {
                return Rect3D.Empty;
            }

            Rect3D textRect = new Rect3D(fontOutlineEntry.GlyphMetrics[glyphMetricIndex].gmfptGlyphOrigin.X,
                                         fontOutlineEntry.GlyphMetrics[glyphMetricIndex].gmfptGlyphOrigin.Y - fontOutlineEntry.GlyphMetrics[glyphMetricIndex].gmfBlackBoxY,
                                         -fontOutlineEntry.Extrusion,
                                         fontOutlineEntry.GlyphMetrics[glyphMetricIndex].gmfBlackBoxX,
                                         fontOutlineEntry.GlyphMetrics[glyphMetricIndex].gmfBlackBoxY,
                                         fontOutlineEntry.Extrusion);

            float currentIncX = fontOutlineEntry.GlyphMetrics[glyphMetricIndex].gmfCellIncX;
            float currentIncY = fontOutlineEntry.GlyphMetrics[glyphMetricIndex].gmfCellIncY;
            
            for (int charIndex = 1; charIndex < charList.Count; charIndex++)
            {
                glyphMetricIndex = (int)charList[charIndex] - (int)fontOutlineEntry.FirstGlyph;
                if (0 > glyphMetricIndex || glyphMetricIndex >= fontOutlineEntry.NumOfGlyphs)
                {
                    return Rect3D.Empty;
                }

                textRect.Union(new Rect3D(fontOutlineEntry.GlyphMetrics[glyphMetricIndex].gmfptGlyphOrigin.X + currentIncX,
                                          fontOutlineEntry.GlyphMetrics[glyphMetricIndex].gmfptGlyphOrigin.Y - fontOutlineEntry.GlyphMetrics[glyphMetricIndex].gmfBlackBoxY + currentIncY,
                                          -fontOutlineEntry.Extrusion,
                                          fontOutlineEntry.GlyphMetrics[glyphMetricIndex].gmfBlackBoxX,
                                          fontOutlineEntry.GlyphMetrics[glyphMetricIndex].gmfBlackBoxY,
                                          fontOutlineEntry.Extrusion));

                currentIncX += fontOutlineEntry.GlyphMetrics[glyphMetricIndex].gmfCellIncX;
                currentIncY += fontOutlineEntry.GlyphMetrics[glyphMetricIndex].gmfCellIncY;
            }

            return textRect;
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the current OpenGL context.
        /// </summary>
        public OpenGL CurrentOpenGLContext
        {
            get { return currentOpenGLContext; }
        }

        public uint FirstFontGlyph
        {
            get { return firstFontGlyph; }
            set { firstFontGlyph = value; }
        }

        public uint NumberOfFontGlyphs
        {
            get { return numOfFontGlyphs; }
            set { numOfFontGlyphs = value; }
        }

        #endregion
    }
}
